SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
7 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
6.1 KB · 79 lines tsx
Raw Blame History
1"use client";2import { useQuery } from "@tanstack/react-query";3import { Download, FileText } from "lucide-react";4import Link from "next/link";5import { useParams } from "next/navigation";6import { useState } from "react";7import { StatusBadge } from "../../page";8import { LinesChart } from "@/components/charts";9import { BenchmarkTable, EquityVsBenchmarks, MetricsGrid, Provenance, RejectedOrders, RollingSharpe, SensitivityHeatmap, ShuffledTrades, StressTable, TradeLedger, Underwater, WalkForward } from "@/components/run-views";10import { Badge, Card, Empty, ErrorBox, PageHeader, Spinner } from "@/components/ui";11import { api } from "@/lib/api";12import { fmtTs } from "@/lib/format";13import type { Run, StrategyMeta } from "@/lib/types";1415export default function RunPage() {16  const { id } = useParams<{ id: string }>();17  const run = useQuery({ queryKey: ["run", id], queryFn: () => api<Run>(`/api/backtests/${id}`), refetchInterval: (q) => (q.state.data && (q.state.data.status === "done" || q.state.data.status === "failed") ? false : 1_500) });18  const strategies = useQuery({ queryKey: ["strategies"], queryFn: () => api<{ items: StrategyMeta[] }>("/api/strategies") });19  const [page, setPage] = useState(0);20  if (run.isLoading) return <Spinner label="Loading run…" />;21  if (run.error) return <ErrorBox error={run.error} />;22  const r = run.data!;23  const meta = strategies.data?.items.find((s) => s.name === r.strategy);24  const a = r.artifacts;25  const m = r.metrics;26  return (27    <>28      <PageHeader29        title={`${meta?.label ?? r.strategy} · ${r.symbol} · ${r.interval_min}m`}30        subtitle={<span className="flex flex-wrap items-center gap-2"><StatusBadge status={r.status} />{meta?.experimental && <Badge tone="warn">experimental — not validated for live use</Badge>}{r.label && <span className="text-fg-muted">{r.label}</span>}{r.start_ts && <span className="num text-fg-dim">{fmtTs(r.start_ts)} → {fmtTs(r.end_ts)}</span>}</span>}31        actions={<>32          <Link href={`/reports/${r.id}`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub"><FileText className="h-3.5 w-3.5" /> Report</Link>33          <a href={`/api/backtests/${r.id}`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub" download={`backtest-${r.id}.json`}><Download className="h-3.5 w-3.5" /> JSON</a>34          <a href={`/api/backtests/${r.id}/trades.csv`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub" download><Download className="h-3.5 w-3.5" /> Trades CSV</a>35        </>}36      />37      {r.status === "failed" && <ErrorBox error={r.error ?? "run failed"} />}38      {(r.status === "queued" || r.status === "running") && <Spinner label={`Backtest ${r.status}… (benchmarks, bootstrap and stress scenarios run in the same job)`} />}39      {r.status === "done" && a && m && (40        <div className="flex flex-col gap-4">41          <Card title="Performance (in-sample window)" subtitle="Net of fees, spread, slippage and impact. Sharpe shows a 95% block-bootstrap interval; the deflated Sharpe is the probability the Sharpe exceeds what the best of N random trials would produce."><MetricsGrid m={m} /></Card>42          <div className="grid gap-4 xl:grid-cols-3">43            <Card title="Equity vs benchmarks" className="xl:col-span-2"><EquityVsBenchmarks a={a} label={r.strategy} /></Card>44            <Card title="Benchmarks" subtitle="Same window, same cost model."><BenchmarkTable a={a} m={m} /></Card>45          </div>46          <div className="grid gap-4 md:grid-cols-2">47            <Card title="Underwater (drawdown)"><Underwater a={a} /></Card>48            <Card title="Rolling Sharpe"><RollingSharpe a={a} /></Card>49          </div>50          {a.holdout ? (51            <Card title="Out-of-sample holdout" subtitle={`Final ${Math.round(Number(r.provenance?.holdout_frac ?? 0) * 100)}% of the data, never used for selection. Starts ${fmtTs(a.holdout.start_ts)}.`}>52              <MetricsGrid m={a.holdout.metrics} compact />53              <div className="mt-3"><LinesChart series={[{ id: "h", label: "holdout equity", color: "#22c55e", points: a.holdout.equity.map((p) => ({ ts: p.ts, value: p.equity })) }]} height={200} /></div>54            </Card>55          ) : <Card title="Out-of-sample holdout"><Empty title="No holdout window">Set a holdout fraction &gt; 0 in the Strategy Lab to reserve a final test slice.</Empty></Card>}56          <Card title="Walk-forward folds"><WalkForward wf={a.walk_forward} /></Card>57          <div className="grid gap-4 md:grid-cols-2">58            <Card title="Parameter sensitivity"><SensitivityHeatmap s={a.sensitivity} /></Card>59            <Card title="Stress scenarios" subtitle="Re-runs with adverse cost assumptions and a simulated exchange outage gap."><StressTable stress={a.stress} base={m} /></Card>60          </div>61          <div className="grid gap-4 md:grid-cols-2">62            <Card title="Shuffled-trades Monte-Carlo" subtitle="Drawdown distribution when round-trip P&Ls are reordered."><ShuffledTrades s={a.shuffled_trades} /></Card>63            <Card title="Rejected orders" subtitle="Blocked by the risk gateway during simulation."><RejectedOrders rejected={a.rejected} /></Card>64          </div>65          <Card title={`Trade ledger (${a.trades.length}${a.trades.length >= 2000 ? ", last 2000 shown" : ""})`}><TradeLedger trades={a.trades} page={page} setPage={setPage} /></Card>66          <Card title="Provenance" subtitle="Everything needed to reproduce this run."><Provenance run={r} /></Card>67          {meta && (68            <Card title="Strategy notes">69              <p className="text-xs text-fg-muted">{meta.hypothesis}</p>70              {meta.failure_modes.length > 0 && <p className="mt-2 text-xs"><span className="text-fg-dim">Known failure modes:</span> {meta.failure_modes.join(" · ")}</p>}71              {meta.references.length > 0 && <p className="mt-1 text-xs"><span className="text-fg-dim">References:</span> {meta.references.join("; ")}</p>}72            </Card>73          )}74        </div>75      )}76    </>77  );78}79